﻿// ReSharper disable SuggestUseVarKeywordEvident, RedundantUsingDirective
using System;
using System.Text;

namespace Plan
{
    /// <summary>
    /// Algorytm symulowanego wyżarzania, wg. slajdów z wykładu dr Lichtensteina
    /// </summary>
    public class SimulatedAnnealingAlgoritm : Plan.IScheduleAlgoritm
    {
        static readonly Random random = new Random(1);
        private double _initialTemperature = 10;
        private double _finalTemperature = 0.01;
        private double _lambda;
        private int _iterationCount = 1000000;
        private CoolDownModes _coolDownMode = CoolDownModes.Geometric;

        public double InitialTemperature
        {
            get { return _initialTemperature; }
            set { _initialTemperature = value; }
        }

        public double Lambda
        {
            get { return _lambda; }
            set { _lambda = value; }
        }

        public CoolDownModes CoolDownMode
        {
            get { return _coolDownMode; }
            set { _coolDownMode = value; }
        }

        public double FinalTemperature
        {
            get { return _finalTemperature; }
            set { _finalTemperature = value; }
        }

        public int IterationCount
        {
            get { return _iterationCount; }
            set { _iterationCount = value; }
        }


        public ScheduleSolution Solve(ScheduleInstance problemInstance)
        {
            problemInstance.Init();
            if (!problemInstance.Solvable)
                throw new ProblemUnsolvable("Problem jest nierozwiązywalny, ponieważ jest za mało jednostek czasowych niż zajęć.");
            var s = new ScheduleSolution(problemInstance);
            int n = s.Perm.Order;
            var best = s.Clone();
            int bestQ = s.PlanShitness();

            s.Perm.Randomize(random); // pierwsze rozw losowe

            
            
            double T = _initialTemperature; // temperatura
            int f1 = s.PlanShitness();

            Console.WriteLine(s.ToString());
            Console.WriteLine("Shittness: {0}", f1);

            SetupLambda(); // ustaw lambda

            int step = 0;
            int stepLimit = _iterationCount/100;
            float progress = 0.0f;

            int i = 0;
            double P = 1.0;
            while (i < _iterationCount && T > _finalTemperature)
            {
                bool switched = false;
                int swapA = random.Next(n);
                int swapB = random.Next(n - 1);
                //int swapA = s.GetRandomIndex(random);
                //int swapB = s.GetRandomIndexExcluding(random, swapA);
                if (swapB >= swapA)
                    swapB++;

                s.Perm.Swap(swapA, swapB);

                var sTry = s.Clone();

                int f2 = sTry.PlanShitness(step == stepLimit);
                if (f2 < bestQ)
                {
                    best = sTry.Clone();
                    bestQ = f2;
                }

                if (f2 < f1)
                {
                    switched = true;
                    P = -1;
                }
                else
                {

                    P = Probability(f2 - f1, T); // f1-f2?
                    if (random.NextDouble() <= P)
                    {
                        s = sTry;
                        switched = true;
                    }
                    else
                    {
                        s.Perm.Swap(swapB, swapA); // unswap...
                    }
                }
                if (switched)
                    f1 = f2;

                CoolDown(ref T);

                if (step == stepLimit)
                {
                    var reportProgressArgs = new ReportProgressArgs(Math.Min(1f, progress), sTry, new ScheduleLogEntry(
                                                                                                   string.Format("{0}\tf={1:0.000000000}\tT={2:0.000000}\tP={3}", i, f2, T,
                                                                                                                 P > 0 ? P.ToString() : "--------"), sTry, 0d, sTry.Shits));
                    InvokeReportProgress(reportProgressArgs);
                    if (reportProgressArgs.Cancel)
                        return best;
                    progress += 0.01f;
                    step = 0;
                }
                step++;
                i++;
            }

            InvokeReportProgress(new ReportProgressArgs(1, best));
            return best;
        }

        static double Probability(double delta, double temperature)
        {
            return Math.Exp(-delta/temperature);
        }

        void CoolDown(ref double T)
        {
            switch(_coolDownMode)
            {
                case CoolDownModes.Geometric:
                    T = T*_lambda;
                    break;
                /*case CoolDownModes.Logarythmic:
                    T = T / (1 + T*_lambda);
                    break;*/
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        void SetupLambda()
        {
            switch (_coolDownMode)
            {
                case CoolDownModes.Geometric:
                    _lambda = Math.Pow((_finalTemperature/_initialTemperature), (1.0/_iterationCount));
                    break;
                /*case CoolDownModes.Logarythmic:
                    _lambda = (_initialTemperature - _finalTemperature * _iterationCount) /
                    (_iterationCount * _initialTemperature * _finalTemperature);
                    break;*/
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        public override string ToString()
        {
            StringBuilder build =
                new StringBuilder(string.Format("Symulowane wyżarzanie T0={0} Tm={1} m={2}", _initialTemperature,
                                                _finalTemperature, _iterationCount));
            return build.ToString();
        }

        // To żeby pokazać postęp.
        public event EventHandler<ReportProgressArgs> ReportProgress;

        private void InvokeReportProgress(ReportProgressArgs e)
        {
            EventHandler<ReportProgressArgs> progress = ReportProgress;
            if (progress != null) progress(this, e);
        }
        public enum CoolDownModes
        {
            Geometric,
            //Logarythmic
        }

    }

    public class ProblemUnsolvable : Exception
    {
        public ProblemUnsolvable(string msg) : base(msg)
        {
        }
    }
}
